
我们在LLVM模块中收集编译单元的所有函数和全局变量。为了简化IR生成过程，可以将前几节中的所有函数包装到代码生成器类中。为了获得一个工作的编译器，还需要定义想要为其生成代码的目标架构，并添加生成代码的通道。我们将在本章和接下来的几章中实现它，先从代码生成器开始。

\mySubsubsection{4.3.1.}{代码生成器}

IR模块包含为编译单元生成大括号内的所有元素。我们在全局级别遍历模块级别的声明，创建全局变量，并调用过程的代码生成。tinylang中的全局变量会映射到llvm::GobalValue类的实例。这种映射保存在全局变量中，可用于过程代码的生成:

\begin{cpp}
void CGModule::run(ModuleDeclaration *Mod) {
    for (auto *Decl : Mod->getDecls()) {
        if (auto *Var =
                llvm::dyn_cast<VariableDeclaration>(Decl)) {
            // Create global variables
            auto *V = new llvm::GlobalVariable(
                *M, convertType(Var->getType()),
                /*isConstant=*/false,
                llvm::GlobalValue::PrivateLinkage, nullptr,
                mangleName(Var));
            Globals[Var] = V;
        } else if (auto *Proc =
            llvm::dyn_cast<ProcedureDeclaration>(Decl)) {
            CGProcedure CGP(*this);
            CGP.run(Proc);
        }
    }
}
\end{cpp}

该模块还包含LLVMContext类，并缓存最常用的LLVM类型。后者需要初始化，例如：对于64位整数类型的初始化:

\begin{cpp}
Int64Ty = llvm::Type::getInt64Ty(getLLVMCtx());
\end{cpp}

CodeGenerator类初始化LLVM IR模块并调用该模块的代码生成，这个类必须了解要为哪个目标架构生成代码。该信息在驱动程序中的llvm::TargetMachine类中:

\begin{cpp}
std::unique_ptr<llvm::Module>
CodeGenerator::run(ModuleDeclaration *Mod,
                   std::string FileName) {
    std::unique_ptr<llvm::Module> M =
        std::make_unique<llvm::Module>(FileName, Ctx);
    M->setTargetTriple(TM->getTargetTriple().getTriple());
    M->setDataLayout(TM->createDataLayout());
    CGModule CGM(M.get());
    CGM.run(Mod);
    return M;
}
\end{cpp}

为了方便使用，必须为代码生成器引入一个工厂方法:

\begin{cpp}
CodeGenerator *
CodeGenerator::create(llvm::LLVMContext &Ctx,
                      llvm::TargetMachine *TM) {
    return new CodeGenerator(Ctx, TM);
}
\end{cpp}

CodeGenerator类提供了一个用于创建IR代码的接口，适合在编译器驱动程序中使用。在集成它之前，需要实现对机器代码生成的支持。

\mySubsubsection{4.3.2.}{初始化目标机型类}

现在，只有目标机型未知。在目标机型上，需要定义为其生成代码的CPU架构。对于每个CPU，都有一些特性会影响代码的生成过程。例如，CPU架构族中的一个较新的CPU可以支持向量指令。通过这些特性，我们可以切换使用向量指令的标志。支持从命令行设置这些选项，LLVM提供了一些支持代码。在Driver类中，可以包含以下头文件:

\begin{cpp}
#include "llvm/CodeGen/CommandFlags.h"
\end{cpp}

这个include将常见的命令行选项添加到编译器驱动程序中。许多LLVM工具也使用这些命令行选项，其好处是为用户提供了一个通用的接口，不过只缺少指定目标三元组的选项:

\begin{cpp}
static llvm::cl::opt<std::string> MTriple(
    "mtriple",
    llvm::cl::desc("Override target triple for module"));
\end{cpp}

让我们定义目标机型:

\begin{enumerate}
\item
要显示错误消息，必须将应用程序的名称传递给该函数:

\begin{cpp}
llvm::TargetMachine *
createTargetMachine(const char *Argv0) {
\end{cpp}

\item
首先，收集命令行提供的所有信息。这些是代码生成器的选项——CPU的名称和应该激活或停用的功能，以及目标的三个值:

\begin{cpp}
    llvm::Triple Triple = llvm::Triple(
        !MTriple.empty()
            ? llvm::Triple::normalize(MTriple)
            : llvm::sys::getDefaultTargetTriple());

    llvm::TargetOptions TargetOptions =
        codegen::InitTargetOptionsFromCodeGenFlags(Triple);
    std::string CPUStr = codegen::getCPUStr();
    std::string FeatureStr = codegen::getFeaturesStr();
\end{cpp}

\item
然后，必须在目标注册表中查找目标。若发生错误，将显示错误消息并退出。一个可能的错误是，用户指定的目标不支持:

\begin{cpp}
    std::string Error;
    const llvm::Target *Target =
        llvm::TargetRegistry::lookupTarget(
         codegen::getMArch(), Triple, Error);

    if (!Target) {
        llvm::WithColor::error(llvm::errs(), Argv0) << Error;
        return nullptr;
    }
\end{cpp}

\item
在Target类的帮助下，可以使用用户请求的所有已知选项来配置目标机型:

\begin{cpp}
    llvm::TargetMachine *TM = Target->createTargetMachine(
        Triple.getTriple(), CPUStr, FeatureStr,
        TargetOptions, std::optional<llvm::Reloc::Model>(
            codegen::getRelocModel()));
    return TM;
}
\end{cpp}

\end{enumerate}

有了目标机型实例，就可以生成相应CPU架构的IR代码。目前缺少的是向汇编文本的转换或目标代码文件的生成，我们将在下一节添加这种支持。

\mySubsubsection{4.3.3.}{生成汇编程文本和目标代码}

在LLVM中，IR代码通过通道运行。每次遍历都只执行一项任务，比如移除无效代码。我们将在第7章中了解更多关于通道的信息。输出汇编代码或目标文件也作为通道实现。让我们为它添加基本支持吧!

需要包含更多的LLVM头文件。首先，需要llvm::legacy::PassManager类来保存向文件发出代码的通道。还希望能够输出LLVM IR代码，因此还需要一个通道来生成它。最后，使用llvm::ToolOutputFile类进行文件操作:

\begin{cpp}
#include "llvm/IR/IRPrintingPasses.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/MC/TargetRegistry.h"
#include "llvm/Pass.h"
#include "llvm/Support/ToolOutputFile.h"
\end{cpp}

还需要另一个用于输出LLVM IR的命令行选项:

\begin{cpp}
static llvm::cl::opt<bool> EmitLLVM(
    "emit-llvm",
    llvm::cl::desc("Emit IR code instead of assembler"),
    llvm::cl::init(false));
\end{cpp}

最后，给输出文件一个名字:

\begin{cpp}
static llvm::cl::opt<std::string>
    OutputFilename("o",
                   llvm::cl::desc("Output filename"),
                   llvm::cl::value_desc("filename"));
\end{cpp}

新emit()方法中的第一个任务是，处理用户在命令行中没有给出的输出文件名。若从标准输入中读取输入，使用减号表示，则将结果输出到标准输出。ToolOutputFile类了解如何处理特殊的文件名:

\begin{cpp}
bool emit(StringRef Argv0, llvm::Module *M,
          llvm::TargetMachine *TM,
          StringRef InputFilename) {
    CodeGenFileType FileType = codegen::getFileType();
    if (OutputFilename.empty()) {
        if (InputFilename == "-") {
            OutputFilename = "-";
        }
\end{cpp}

否则，删除输入文件名的可能扩展名，并根据用户给出的命令行选项追加.ll、.s或.o作为扩展名。FileType选项在llvm/CodeGen/CommandFlags.inc头文件中定义，在前面包含了它。这个选项不支持发出IR代码，所以添加了new-emit-llvm选项，只有在与汇编文件类型一起使用时才有效:

\begin{cpp}
    else {
        if (InputFilename.endswith(".mod"))
            OutputFilename =
                InputFilename.drop_back(4).str();
        else
            OutputFilename = InputFilename.str();
        switch (FileType) {
            case CGFT_AssemblyFile:
                OutputFilename.append(EmitLLVM ? ".ll" : ".s");
                break;
            case CGFT_ObjectFile:
                OutputFilename.append(".o");
                break;
            case CGFT_Null:
                OutputFilename.append(".null");
                break;
        }
    }
}
\end{cpp}

一些平台区分文本和二进制文件，所以必须在打开输出文件时提供正确的标志:

\begin{cpp}
std::error_code EC;
sys::fs::OpenFlags OpenFlags = sys::fs::OF_None;
if (FileType == CGFT_AssemblyFile)
    OpenFlags |= sys::fs::OF_TextWithCRLF;
auto Out = std::make_unique<llvm::ToolOutputFile>(
    OutputFilename, EC, OpenFlags);
if (EC) {
    WithColor::error(llvm::errs(), Argv0)
        << EC.message() << '\n';
    return false;
}
\end{cpp}

现在，可以将所需的通道添加到PassManager。TargetMachine类有一个工具方法，用于添加所请求的类，所以只需要检查用户是否请求输出LLVM IR代码即可:

\begin{cpp}
    legacy::PassManager PM;
    if (FileType == CGFT_AssemblyFile && EmitLLVM) {
        PM.add(createPrintModulePass(Out->os()));
    } else {
        if (TM->addPassesToEmitFile(PM, Out->os(), nullptr,
                                    FileType)) {
            WithColor::error(llvm::errs(), Argv0)
                << "No support for file type\n";
            return false;
        }
    }
\end{cpp}

所有这些准备工作完成后，生成文件可以归结为一个函数调用:

\begin{cpp}
PM.run(*M);
\end{cpp}

若没有显式地请求保留该文件，ToolOutputFile类会自动删除该文件。这使得错误处理更容易，因为可能有许多地方需要处理错误。我们成功地生成了代码，所以想保留这个文件:

\begin{cpp}
Out->keep();
\end{cpp}

最后，必须向调用者报告成功与否:

\begin{cpp}
    return true;
}
\end{cpp}

使用llvm::Module调用emit()方法(通过调用CodeGenerator类创建了该方法)，将按要求发出代码。假设将tinylang中的最大公约数算法存储在Gcd.mod文件中:

\begin{shell}
MODULE Gcd;

PROCEDURE GCD(a, b: INTEGER) : INTEGER;
VAR t: INTEGER;
BEGIN
    IF b = 0 THEN
        RETURN a;
    END;
    WHILE b # 0 DO
    t := a MOD b;
    a := b;
    b := t;
    END;
    RETURN a;
END GCD;

END Gcd.
\end{shell}

为了把这个翻译成Gcd.o目标文件，需要输入以下内容:

\begin{shell}
$ tinylang --filetype=obj Gcd.mod
\end{shell}

若想直接在屏幕上检查生成的IR代码，请输入以下命令:

\begin{shell}
$ tinylang --filetype=asm --emit-llvm -o - Gcd.mod
\end{shell}

以tinylang的当前实现状态，无法在tinylang中创建完整的程序，可以使用一个名为callgcd.c的C程序来测试生成的目标文件。注意，调用GCD函数时已经修改了名称:

\begin{cpp}
#include <stdio.h>

extern long _t3Gcd3GCD(long, long);

int main(int argc, char *argv[]) {
    printf("gcd(25, 20) = %ld\n", _t3Gcd3GCD(25, 20));
    printf("gcd(3, 5) = %ld\n", _t3Gcd3GCD(3, 5));
    printf("gcd(21, 28) = %ld\n", _t3Gcd3GCD(21, 28));
    return 0;
}
\end{cpp}

要使用clang编译并运行整个应用程序，需要输入以下命令:

\begin{shell}
$ tinylang --filetype=obj Gcd.mod
$ clang callgcd.c Gcd.o -o gcd
$ gcd
\end{shell}

让我们好好庆祝一下吧!至此，通过读取语言源码，并生成了汇编代码或目标文件，我们已经创建了一个完整的编译器。


















